﻿/*
	Copyright (c) 2013 Andrew Meyer

	Permission is hereby granted, free of charge, to any person obtaining a copy of this software
	and associated documentation files (the "Software"), to deal in the Software without
	restriction, including without limitation the right to use, copy, modify, merge, publish,
	distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
	Software is furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all copies or
	substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
	BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
	NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
	DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Media.Media3D;

namespace Ascend
{
	/// <summary>
	/// Abstract implementation of an object that can be positioned, rotated, and scaled in three
	/// dimensions. This class cannot be inherited externally.
	/// </summary>
	public abstract class TransformableObject : DependencyObject
	{
		#region Lifecycle
		internal TransformableObject()
		{
			Data = new Model3DGroup();

			SetupTransforms();
		}
		#endregion

		#region Private Members
		private TranslateTransform3D _translateTransform;
		private RotateTransform3D _rotateTransformX;
		private RotateTransform3D _rotateTransformY;
		private RotateTransform3D _rotateTransformZ;
		private ScaleTransform3D _scaleTransform;
		#endregion

		#region Internal Members
		/// <summary>
		/// The container to which this TransformableObject's transformations are applied.
		/// </summary>
		internal Model3DGroup Data
		{
			get
			{
				return (Model3DGroup)GetValue(DataProperty);
			}
			
			set
			{
				SetValue(DataProperty, value);
			}
		}

		private static readonly DependencyProperty DataProperty =
			DependencyProperty.Register("Data", typeof(Model3DGroup), typeof(TransformableObject));
		#endregion

		#region Dependency Properties
		/// <summary>
		/// Gets or sets this Ascend.TransformableObject's X coordinate.
		/// </summary>
		public double X
		{
			get
			{
				return (double)GetValue(XProperty);
			}
			
			set
			{
				SetValue(XProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.TransformableObject.X dependency property.
		/// </summary>
		public static readonly DependencyProperty XProperty =
			DependencyProperty.Register("X", typeof(double), typeof(TransformableObject),
			new PropertyMetadata(0.0, OnXChanged));

		private static void OnXChanged(DependencyObject sender,
			DependencyPropertyChangedEventArgs e)
		{
			TransformableObject me = sender as TransformableObject;

			me._translateTransform.OffsetX = (double)e.NewValue;
			me.Position = new Point3D(me.X, me.Y, me.Z);
		}


		/// <summary>
		/// Gets or sets this Ascend.TransformableObject's Y coordinate.
		/// </summary>
		public double Y
		{
			get
			{
				return (double)GetValue(YProperty);
			}
			
			set
			{
				SetValue(YProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.TransformableObject.Y dependency property.
		/// </summary>
		public static readonly DependencyProperty YProperty =
			DependencyProperty.Register("Y", typeof(double), typeof(TransformableObject),
			new PropertyMetadata(0.0, OnYChanged));

		private static void OnYChanged(DependencyObject sender,
			DependencyPropertyChangedEventArgs e)
		{
			TransformableObject me = sender as TransformableObject;

			me._translateTransform.OffsetY = (double)e.NewValue;
			me.Position = new Point3D(me.X, me.Y, me.Z);
		}


		/// <summary>
		/// Gets or sets this Ascend.TransformableObject's Z coordinate.
		/// </summary>
		public double Z
		{
			get
			{
				return (double)GetValue(ZProperty);
			}
			
			set
			{
				SetValue(ZProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.TransformableObject.Z dependency property.
		/// </summary>
		public static readonly DependencyProperty ZProperty =
			DependencyProperty.Register("Z", typeof(double), typeof(TransformableObject),
			new PropertyMetadata(0.0, OnZChanged));

		private static void OnZChanged(DependencyObject sender,
			DependencyPropertyChangedEventArgs e)
		{
			TransformableObject me = sender as TransformableObject;

			me._translateTransform.OffsetZ = (double)e.NewValue;
			me.Position = new Point3D(me.X, me.Y, me.Z);
		}


		/// <summary>
		/// Gets or sets this Ascend.TransformableObject's three-dimensional position.
		/// </summary>
		public Point3D Position
		{
			get { return (Point3D)GetValue(PositionProperty); }
			set { SetValue(PositionProperty, value); }
		}

		/// <summary>
		/// Identifies the Ascend.TransformableObject.Position dependency property.
		/// </summary>
		public static readonly DependencyProperty PositionProperty =
			DependencyProperty.Register("Position", typeof(Point3D), typeof(TransformableObject),
			new PropertyMetadata(new Point3D(0.0, 0.0, 0.0), OnPositionChanged));

		private static void OnPositionChanged(DependencyObject sender,
			DependencyPropertyChangedEventArgs e)
		{
			TransformableObject me = sender as TransformableObject;
			Point3D position = (Point3D)e.NewValue;

			me.X = position.X;
			me.Y = position.Y;
			me.Z = position.Z;
		}


		/// <summary>
		/// Gets or sets this Ascend.TransformableObject's resting X-axis angle (roll) in degrees.
		/// </summary>
		/// <remarks>
		/// The rotation convention used by Ascend is yaw-pitch-roll / YZX.
		/// </remarks>
		public double AngleX
		{
			get
			{
				return (double)GetValue(AngleXProperty);
			}
			
			set
			{
				SetValue(AngleXProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.TransformableObject.AngleX dependency property.
		/// </summary>
		public static readonly DependencyProperty AngleXProperty =
			DependencyProperty.Register("AngleX", typeof(double), typeof(TransformableObject),
			new PropertyMetadata(0.0, OnAngleXChanged));

		private static void OnAngleXChanged(DependencyObject sender,
			DependencyPropertyChangedEventArgs e)
		{
			TransformableObject me = sender as TransformableObject;

			((AxisAngleRotation3D)me._rotateTransformX.Rotation).Angle = (double)e.NewValue;
			me.Rotation = new Angle3D(me.AngleX, me.AngleY, me.AngleZ);
		}


		/// <summary>
		/// Gets or sets this Ascend.TransformableObject's resting Y-axis angle (yaw) in degrees.
		/// </summary>
		/// <remarks>
		/// The rotation convention used by Ascend is yaw-pitch-roll / YZX.
		/// </remarks>
		public double AngleY
		{
			get
			{
				return (double)GetValue(AngleYProperty);
			}
			
			set
			{
				SetValue(AngleYProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.TransformableObject.AngleY dependency property.
		/// </summary>
		public static readonly DependencyProperty AngleYProperty =
			DependencyProperty.Register("AngleY", typeof(double), typeof(TransformableObject),
			new PropertyMetadata(0.0, OnAngleYChanged));

		private static void OnAngleYChanged(DependencyObject sender,
			DependencyPropertyChangedEventArgs e)
		{
			TransformableObject me = sender as TransformableObject;

			((AxisAngleRotation3D)me._rotateTransformY.Rotation).Angle = (double)e.NewValue;
			me.Rotation = new Angle3D(me.AngleX, me.AngleY, me.AngleZ);
		}


		/// <summary>
		/// Gets or sets this Ascend.TransformableObject's resting Z-axis angle (pitch) in degrees.
		/// </summary>
		/// <remarks>
		/// The rotation convention used by Ascend is yaw-pitch-roll / YZX.
		/// </remarks>
		public double AngleZ
		{
			get
			{
				return (double)GetValue(AngleZProperty);
			}
			
			set
			{
				SetValue(AngleZProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.TransformableObject.AngleZ dependency property.
		/// </summary>
		public static readonly DependencyProperty AngleZProperty =
			DependencyProperty.Register("AngleZ", typeof(double), typeof(TransformableObject),
			new PropertyMetadata(0.0, OnAngleZChanged));

		private static void OnAngleZChanged(DependencyObject sender,
			DependencyPropertyChangedEventArgs e)
		{
			TransformableObject me = sender as TransformableObject;

			((AxisAngleRotation3D)me._rotateTransformZ.Rotation).Angle = (double)e.NewValue;
			me.Rotation = new Angle3D(me.AngleX, me.AngleY, me.AngleZ);
		}


		/// <summary>
		/// Gets or sets this Ascend.TransformableObject's three-dimensional rotation.
		/// </summary>
		public Angle3D Rotation
		{
			get { return (Angle3D)GetValue(RotationProperty); }
			set { SetValue(RotationProperty, value); }
		}

		/// <summary>
		/// Identifies the Ascend.TransformableObject.Rotation dependency property.
		/// </summary>
		public static readonly DependencyProperty RotationProperty =
			DependencyProperty.Register("Rotation", typeof(Angle3D), typeof(TransformableObject),
			new PropertyMetadata(new Angle3D(0.0, 0.0, 0.0), OnRotationChanged));

		private static void OnRotationChanged(DependencyObject sender,
			DependencyPropertyChangedEventArgs e)
		{
			TransformableObject me = sender as TransformableObject;
			Angle3D rotation = (Angle3D)e.NewValue;

			me.AngleX = rotation.X;
			me.AngleY = rotation.Y;
			me.AngleZ = rotation.Z;
		}


		/// <summary>
		/// Gets or sets this Ascend.TransformableObject's X-axis scale factor.
		/// </summary>
		public double ScaleX
		{
			get
			{
				return (double)GetValue(ScaleXProperty);
			}
			
			set
			{
				SetValue(ScaleXProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.TransformableObject.ScaleX dependency property.
		/// </summary>
		public static readonly DependencyProperty ScaleXProperty =
			DependencyProperty.Register("ScaleX", typeof(double), typeof(TransformableObject),
			new PropertyMetadata(1.0, OnScaleXChanged));

		private static void OnScaleXChanged(DependencyObject sender,
			DependencyPropertyChangedEventArgs e)
		{
			TransformableObject me = sender as TransformableObject;

			me._scaleTransform.ScaleX = (double)e.NewValue;
			me.Scale = new Scale3D(me.ScaleX, me.ScaleY, me.ScaleZ);
		}


		/// <summary>
		/// Gets or sets this Ascend.TransformableObject's Y-axis scale factor.
		/// </summary>
		public double ScaleY
		{
			get
			{
				return (double)GetValue(ScaleYProperty);
			}
			
			set
			{
				SetValue(ScaleYProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.TransformableObject.ScaleY dependency property.
		/// </summary>
		public static readonly DependencyProperty ScaleYProperty =
			DependencyProperty.Register("ScaleY", typeof(double), typeof(TransformableObject),
			new PropertyMetadata(1.0, OnScaleYChanged));

		private static void OnScaleYChanged(DependencyObject sender,
			DependencyPropertyChangedEventArgs e)
		{
			TransformableObject me = sender as TransformableObject;

			me._scaleTransform.ScaleY = (double)e.NewValue;
			me.Scale = new Scale3D(me.ScaleX, me.ScaleY, me.ScaleZ);
		}


		/// <summary>
		/// Gets or sets this Ascend.TransformableObject's Z-axis scale factor.
		/// </summary>
		public double ScaleZ
		{
			get
			{
				return (double)GetValue(ScaleZProperty);
			}
			
			set
			{
				SetValue(ScaleZProperty, value);
			}
		}

		/// <summary>
		/// Identifies the Ascend.TransformableObject.ScaleZ dependency property.
		/// </summary>
		public static readonly DependencyProperty ScaleZProperty =
			DependencyProperty.Register("ScaleZ", typeof(double), typeof(TransformableObject),
			new PropertyMetadata(1.0, OnScaleZChanged));

		private static void OnScaleZChanged(DependencyObject sender,
			DependencyPropertyChangedEventArgs e)
		{
			TransformableObject me = sender as TransformableObject;

			me._scaleTransform.ScaleZ = (double)e.NewValue;
			me.Scale = new Scale3D(me.ScaleX, me.ScaleY, me.ScaleZ);
		}


		/// <summary>
		/// Gets or sets this Ascend.TransformableObject's three-dimensional scale.
		/// </summary>
		public Scale3D Scale
		{
			get { return (Scale3D)GetValue(ScaleProperty); }
			set { SetValue(ScaleProperty, value); }
		}

		/// <summary>
		/// Identifies the Ascend.TransformableObject.Scale dependency property.
		/// </summary>
		public static readonly DependencyProperty ScaleProperty =
			DependencyProperty.Register("Scale", typeof(Scale3D), typeof(TransformableObject),
			new PropertyMetadata(new Scale3D(1.0), OnScaleChanged));

		private static void OnScaleChanged(DependencyObject sender,
			DependencyPropertyChangedEventArgs e)
		{
			TransformableObject me = sender as TransformableObject;
			Scale3D scale = (Scale3D)e.NewValue;

			me.ScaleX = scale.X;
			me.ScaleY = scale.Y;
			me.ScaleZ = scale.Z;
		}
		#endregion

		#region Public Properties
		/// <summary>
		/// Gets or sets the name of this Ascend.TransformableObject.
		/// </summary>
		public string Name
		{
			get;
			set;
		}

		internal TransformableObject _transformParent;
		/// <summary>
		/// Gets the Ascend.TransformableObject to which this Ascend.TransformableObject is
		/// relatively transformed.
		/// </summary>
		public TransformableObject TransformParent
		{
			get
			{
				return _transformParent;
			}
		}

		/// <summary>
		/// Gets a read-only collection of Ascend.TransformableObject instances that are transformed
		/// relative to this Ascend.TransformableObject.
		/// </summary>
		public abstract ReadOnlyCollection<TransformableObject> TransformChildren
		{
			get;
		}

		/// <summary>
		/// Gets a copy of this Ascend.TransformableObject's transform relative to its transform
		/// parent.
		/// </summary>
		public Transform3D LocalTransform
		{
			get
			{
				return Data.Transform.Clone();
			}
		}

		/// <summary>
		/// Gets a copy of this Ascend.TransformableObject's transform relative to its furthest
		/// ancestor.
		/// </summary>
		public Transform3D GlobalTransform
		{
			get
			{
				Transform3DGroup ret = new Transform3DGroup();
				ret.Children.Add(LocalTransform);

				if (TransformParent != null)
				{
					ret.Children.Add(TransformParent.GlobalTransform);
				}

				return ret;
			}
		}

		/// <summary>
		/// Gets the axis-aligned bounding box of this Ascend.TransformableObject and its
		/// descendants.
		/// </summary>
		public Rect3D Bounds
		{
			get
			{
				return Data.Bounds;
			}
		}
		#endregion

		#region Public Methods
		/// <summary>
		/// Gets a read-only collection of this Ascend.TransformableObject's ancestors.
		/// </summary>
		/// <returns>
		/// Returns a read-only collection of Ascend.TransformableObject instances that includes
		/// this Ascend.TransformableObject's transform parent in addition to the transform parent's
		/// ancestors.
		/// </returns>
		public ReadOnlyCollection<TransformableObject> GetAncestors()
		{
			List<TransformableObject> ancestors = new List<TransformableObject>();

			if (TransformParent != null)
			{
				ancestors.Add(TransformParent);
				ancestors.AddRange(TransformParent.GetAncestors());
			}

			return ancestors.AsReadOnly();
		}

		/// <summary>
		/// Gets a read-only collection of this Ascend.TransformableObject's descendants.
		/// </summary>
		/// <returns>
		/// Returns a read-only collection of Ascend.TransformableObject instances that includes
		/// this Ascend.TransformableObject's children in addition to each child's descendants.
		/// </returns>
		public ReadOnlyCollection<TransformableObject> GetDescendants()
		{
			List<TransformableObject> descendants = new List<TransformableObject>();

			foreach (TransformableObject child in TransformChildren)
			{
				descendants.Add(child);
				descendants.AddRange(child.GetDescendants());
			}

			return descendants.AsReadOnly();
		}

		/// <summary>
		/// Creates a deep copy of this Ascend.TransformableObject, including deep copies of all
		/// transform children.
		/// </summary>
		/// <returns>
		/// Returns the deep copy.
		/// </returns>
		public abstract TransformableObject Clone();

		/// <summary>
		/// Creates a string representation of this Ascend.TransformableObject.
		/// </summary>
		/// <returns>
		/// Returns a string containing this Ascend.TranformableObject's name, if any, and type.
		/// </returns>
		public override string ToString()
		{
			return (string.IsNullOrWhiteSpace(Name) ? string.Empty : Name + " ")
				+ "(" + GetType().Name + ")";
		}
		#endregion

		#region Protected Methods
		internal virtual void ClonePose(TransformableObject clone)
		{
			clone.Position = Position;
			clone.Rotation = Rotation;
			clone.Scale = Scale;
		}
		#endregion

		#region Private Methods
		private void SetupTransforms()
		{
			// Setup translation and scale transforms
			_translateTransform = new TranslateTransform3D();
			_scaleTransform = new ScaleTransform3D();

			// Setup rotation transform using YZX Euler angles
			Transform3DGroup rotateTransform = new Transform3DGroup();
			_rotateTransformX =
				new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(1, 0, 0), 0));
			_rotateTransformY =
				new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(0, 1, 0), 0));
			_rotateTransformZ =
				new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(0, 0, 1), 0));
			rotateTransform.Children.Add(_rotateTransformY); // yaw
			rotateTransform.Children.Add(_rotateTransformZ); // pitch
			rotateTransform.Children.Add(_rotateTransformX); // roll

			// Setup total transform (order of children matters)
			Data.Transform = new Transform3DGroup();
			((Transform3DGroup)Data.Transform).Children.Add(_scaleTransform);
			((Transform3DGroup)Data.Transform).Children.Add(rotateTransform);
			((Transform3DGroup)Data.Transform).Children.Add(_translateTransform);
		}
		#endregion
	}
}
